﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Printing;
using System.Windows.Forms;
using System.Linq;

namespace SuperListEx
{
    public class PrintTable
    {
        #region helper classes
        private class PrintColumn
        {
            public ColumnItemValueAccessor value;
            public ColumnItemValueAccessorWithParameter value2;
            public object o;
            public string name;
            public int width;
            public object valueParameter;
        }

        private class PrintSettingsCache
        {
            public int FontOffset; //the height of the printed font
            public int LeftMargin; //the location of the left margin
            public int RightMargin; //the location of the right margin
            public int TopMargin; //location of the top margin
            public int paperwidth; //the width of the paper
            public int PageNumber; //currnt page number
            public int currentPartialPage; //when printing tables, which part of the table is being worked
            public int NumberOfPageParts; //when printing tables, how many parts there are to the table
            public bool PrintOnlyOne = true; //flag
            public int CurrentXOffset = 0;
            public int PrintableHeight;
            public int MarginBoundWidth;
            public int BottomMargin;
            public int PageRightLocation;
            public int PageTop;
            public int BottomMarginLocation;
            public int workItem = 0;
        }
        #endregion

        #region vars
        private PrintSettingsCache _printSettings;
        private PrintDocument printDocument;
        private static PageSettings pageSettings;
        private static PrinterSettings printerSettings;
        
        Font printFont;

        private Dictionary<PrintColumn, int> PageColumns; //used when printing columns   
        private List<object> inProgress; //used when printing columns
        public List<object> _toPrint;
        ListControl printfrom;      
        #endregion

        #region properties
        /// <summary>
        /// Header image if appropriate
        /// </summary>
        public Image HeaderImage
        {
            get;
            set;
        }

        /// <summary>
        /// The transaction to print
        /// </summary>
        public List<object> ToPrint
        {
            get;
            set;
        }
        #endregion

        #region Constructors
        /// <summary>
        /// Default constructor
        /// </summary>
        public PrintTable(ListControl control) : this()
        {
            printfrom = control;
            _toPrint = null;

            printDocument = new PrintDocument();
            printDocument.PrinterSettings = printerSettings;
            printDocument.DefaultPageSettings = pageSettings;
            printDocument.PrintPage += new PrintPageEventHandler(printDocument_PrintPage);
        }

        private PrintTable()
        {
            if (pageSettings == null)
            {
                pageSettings = new PageSettings();
                pageSettings.Margins.Top = 100;
                pageSettings.Margins.Bottom = 100;
                pageSettings.Margins.Left = 100;
                pageSettings.Margins.Right = 100;
            }

            if (printerSettings == null)
            {
                printerSettings = new PrinterSettings();
            }
        }
        #endregion

        #region Setttings
        /// <summary>
        /// Will display the print settings dialog box
        /// </summary>
        public static void showPageSettings()
        {
            new PrintTable(); //init object

            PageSetupDialog pageSetupDialog = new PageSetupDialog();
            pageSetupDialog.PageSettings = pageSettings;
            pageSetupDialog.PrinterSettings = printerSettings;
            pageSetupDialog.AllowOrientation = true;
            pageSetupDialog.AllowMargins = true;
            if (pageSetupDialog.ShowDialog() == DialogResult.OK)
            {
                pageSettings = pageSetupDialog.PageSettings;
                printerSettings = pageSetupDialog.PrinterSettings;
            }
        }
        #endregion

        #region print methods
        /// <summary>
        /// Prints the transaction
        /// </summary>
        public void Print()
        {
            if (ToPrint.Count < 1) return;
            PrintDialog dialog = new PrintDialog();

            printDocument.DefaultPageSettings = pageSettings;
            dialog.Document = printDocument;
            printFont = new Font("Times New Roman", 12, FontStyle.Regular);

            if (dialog.ShowDialog() == DialogResult.OK)
            {
                printDocument.Print();
            }
        }

        /// <summary>
        /// Shows the print preview dialog
        /// </summary>
        public void PrintPreview()
        {
            if (ToPrint.Count < 1) return;
            PrintPreviewDialog dialog = new PrintPreviewDialog();

            printDocument.DefaultPageSettings = pageSettings;
            dialog.Document = printDocument;
            printFont = new Font("Times New Roman", 12, FontStyle.Regular);
            
            dialog.ShowIcon = false;
            dialog.Document = printDocument;
            dialog.ShowDialog();
        }
        #endregion

        #region print events
        /// <summary>
        /// Event handler for printing
        /// </summary>
        void printDocument_PrintPage(object sender, PrintPageEventArgs e)
        {
            if (_printSettings == null)
            {//init print settings
                if (_toPrint == null)
                    QueuePrintLines();
                _printSettings = new PrintSettingsCache()
                {          
                    FontOffset = (int)printFont.GetHeight(e.Graphics),
                    LeftMargin = e.MarginBounds.Left,
                    RightMargin = e.MarginBounds.Right,
                    TopMargin = e.MarginBounds.Top,
                    paperwidth = e.PageSettings.PaperSize.Width,
                    PageNumber = 0,
                    currentPartialPage = 1 ,                    
                    PrintOnlyOne = ToPrint.Count == 1,
                    PrintableHeight = -1,
                    MarginBoundWidth = e.MarginBounds.Width,
                    BottomMargin = e.PageSettings.Margins.Bottom,
                    PageRightLocation = e.PageBounds.Right,
                    PageTop = e.PageBounds.Top,
                    BottomMarginLocation = e.MarginBounds.Bottom
                };
                if (PageColumns != null)
                    _printSettings.NumberOfPageParts = (int)(from s in PageColumns orderby s.Value descending select s.Value).Take(1).First();
            }


            AddPageHeader(sender, e);
            addPageNumber(sender, e);
            PrintMany(sender, e);

            if (!e.HasMorePages)//clear cache
            {
                _printSettings = null;
                _toPrint = null;
            }
        }       

        /// <summary>
        /// Print routine for a list of items
        /// </summary>
        void PrintMany(object sender, PrintPageEventArgs e)
        {
            int y = AddColumnHeaders(sender, e, _printSettings.FontOffset);
            if (_printSettings.PrintableHeight == -1)
            {//first run need to set
                _printSettings.PrintableHeight = (int)e.PageSettings.PrintableArea.Height - y - e.PageSettings.Margins.Bottom;
            }

            int allocatedHeight = 0;

            if (_toPrint.Count == 0 && inProgress.Count == 0) //see if work is needed
            {
                e.HasMorePages = false;
                return;
            }

            var currPage = from C
                           in PageColumns
                           where C.Value == _printSettings.currentPartialPage
                           select C;

            while (allocatedHeight < _printSettings.PrintableHeight)
            {
                object currWork = GetWorkItem();
                if (currWork == null) break; //nothing to do

                foreach (var v in currPage)
                {
                    object value;
                    if (v.Key.value != null)
                    {
                        value = v.Key.value(currWork);
                    }

                    else
                    {
                        value = v.Key.value2(currWork, v.Key.valueParameter);
                    }

                    //draw vertical line to left side of text
                    e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin + _printSettings.CurrentXOffset, y, _printSettings.LeftMargin + _printSettings.CurrentXOffset, y + _printSettings.FontOffset);
                    
                    //print value
                    if (value is string)
                        e.Graphics.DrawString(value.ToString(), printFont, Brushes.Black, new RectangleF(_printSettings.LeftMargin + _printSettings.CurrentXOffset, y, v.Key.width, _printSettings.FontOffset));
                    else if (value is Image)
                        e.Graphics.DrawImage((Image)value, new RectangleF(_printSettings.LeftMargin + _printSettings.CurrentXOffset, y, v.Key.width, _printSettings.CurrentXOffset));

                    //omit left line as it will be drawn by next iteration
                    _printSettings.CurrentXOffset += v.Key.width;
                }

                //close box, first last vertical line
                e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin + _printSettings.CurrentXOffset, y, _printSettings.LeftMargin + _printSettings.CurrentXOffset, y + _printSettings.FontOffset);
                //draw the line that goes across the bottom
                e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin, y + _printSettings.FontOffset, _printSettings.LeftMargin + _printSettings.CurrentXOffset, y + _printSettings.FontOffset);

                allocatedHeight += _printSettings.FontOffset;
                y += _printSettings.FontOffset;
                AddItemExtension(e, currWork, ref y, ref allocatedHeight);
                _printSettings.CurrentXOffset = 0;
            }

            if (_printSettings.currentPartialPage < _printSettings.NumberOfPageParts || _toPrint.Count > 0)
            {
                e.HasMorePages = true;

                if (_printSettings.currentPartialPage == _printSettings.NumberOfPageParts)
                {
                    _printSettings.currentPartialPage = 1; //reset counter
                    _printSettings.workItem = 0;
                    inProgress.Clear();
                }
                else
                {
                    _printSettings.currentPartialPage++;
                    _printSettings.workItem = 0;
                }
            }
            else
                e.HasMorePages = false;
        }

        /// <summary>
        /// If a line item has extended text, adds it below the item
        /// </summary>
        /// <param name="e">Print graphics</param>
        /// <param name="currWork">The item being worked</param>
        /// <param name="currY">Current Y Offset</param>
        /// <param name="AllocatedHeight">Current used height</param>
        private void AddItemExtension(PrintPageEventArgs e, object currWork, ref int currY, ref int AllocatedHeight)
        {
            //handle preview i.e. transaction note
            if (printfrom.SectionFactory is MySectionFactory)
            {
                MySectionFactory factory = printfrom.SectionFactory as MySectionFactory;
                string text = factory.getText(currWork).ToString();
                string[] toPrint;

                if (text != "")
                {
                    if (text.Contains(Environment.NewLine))
                        toPrint = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
                    else
                        toPrint = new string[] { text };

                    foreach (string s in toPrint)
                    {
                        //draw vertical line to left side of text
                        e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin, currY, _printSettings.LeftMargin, currY + _printSettings.FontOffset);
                        //print text
                        if (_printSettings.currentPartialPage == 1)
                        {//only print text on first page
                            e.Graphics.DrawString(text, printFont, Brushes.Black, new RectangleF(_printSettings.LeftMargin, currY, _printSettings.MarginBoundWidth, _printSettings.FontOffset));
                        }
                        //close box, first last vertical line
                        e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin + _printSettings.CurrentXOffset, currY, _printSettings.LeftMargin + _printSettings.CurrentXOffset, currY + _printSettings.FontOffset);
                        AllocatedHeight += _printSettings.FontOffset;
                        currY += _printSettings.FontOffset;
                    }
                    //draw the line that goes across the bottom
                    e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin, currY, _printSettings.LeftMargin + _printSettings.CurrentXOffset, currY);
                }
            }
        }

        /// <summary>
        /// Adds the column header to the top of the printed page and returns the appropriate offset to start printing the rest of the page
        /// </summary>
        /// <param name="fontOffset">The Height of the font</param>
        private int AddColumnHeaders(object sender, PrintPageEventArgs e, int fontOffset)
        {
            int headerWidth = 0;
            int y = (int)_printSettings.TopMargin;

            var currPage = from C
                           in PageColumns
                           where C.Value == _printSettings.currentPartialPage
                           select C;            

            foreach (var v in currPage)
            {
                //draw vertical line to left side of text
                e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin + headerWidth, y, _printSettings.LeftMargin + headerWidth, y + fontOffset);
                //draw text
                e.Graphics.DrawString(v.Key.name, printFont, Brushes.Black, new RectangleF(_printSettings.LeftMargin + headerWidth, y, v.Key.width, fontOffset));
                //omit left line as it will be drawn by next iteration
                headerWidth += v.Key.width;
            }

            //close box, first last vertical line
            e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin + headerWidth, y, _printSettings.LeftMargin + headerWidth, y + fontOffset);
            //draw the line that goes across the top
            e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin, y, _printSettings.LeftMargin + headerWidth, y);
            //draw the line that goes across the bottom
            e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin, y + fontOffset, _printSettings.LeftMargin + headerWidth, y + fontOffset);
            return y + fontOffset;
        }

        /// <summary>
        /// Adds the page number at the bottom of the print document
        /// </summary>
        private void addPageNumber(object sender, PrintPageEventArgs e)
        {
            int centerLocation;
            string pageNumberString = "Page: " + ++_printSettings.PageNumber;

            Rectangle pageNumberLocation = new Rectangle();
            pageNumberLocation.Width = _printSettings.paperwidth;
            pageNumberLocation.Height = _printSettings.BottomMargin;

            centerLocation = (int)(_printSettings.paperwidth / 2) - (int)(e.Graphics.MeasureString(pageNumberString, new Font("Times New Roman", 12, FontStyle.Regular)).Width / 2);
            pageNumberLocation.Location = new Point(centerLocation, _printSettings.BottomMarginLocation);

            e.Graphics.DrawString(pageNumberString, new Font("Times New Roman", 12, FontStyle.Regular), Brushes.Black, pageNumberLocation, new StringFormat());
        }

        /// <summary>
        /// Adds the page header to the top of the page and adjusts margins as needed
        /// </summary>
        private void AddPageHeader(object sender, PrintPageEventArgs e)
        {
            if (HeaderImage == null) return;

            //construct rectangle for image to show in (letterhead)
            Rectangle headerbox;
            int ImagePad = 10; //padding between image and text
            int pagePad = 20; //padding between top of page and header

            if (_printSettings.PageNumber == 0)
            {
                headerbox = new Rectangle(_printSettings.LeftMargin, _printSettings.PageTop + pagePad, _printSettings.paperwidth - _printSettings.LeftMargin - (_printSettings.PageRightLocation - _printSettings.RightMargin), _printSettings.TopMargin + 35);

                //resize print area to accomidate
                _printSettings.TopMargin = _printSettings.PageTop + headerbox.Height + ImagePad + pagePad;
            }
            else //bounds get adjusted after first run, so now just use it unchanged (except padding added above
                headerbox = new Rectangle(_printSettings.LeftMargin, _printSettings.PageTop + pagePad, _printSettings.paperwidth - _printSettings.LeftMargin - (_printSettings.PageRightLocation - _printSettings.RightMargin), _printSettings.TopMargin - ImagePad - pagePad);
            
            //draw image in rectangle
            e.Graphics.DrawImage(HeaderImage, headerbox);            
        }

        /// <summary>
        /// Returns the current work item to print
        /// </summary>
        /// <returns></returns>
        private object GetWorkItem()
        {
             //used to keep track of whick transaction is current for pages 2 -x
            object currWork = null;

            if (_printSettings.currentPartialPage == 1)
            {
                if (_toPrint.Count > 0)
                {
                    currWork = _toPrint[0];
                    _toPrint.Remove(currWork);
                    inProgress.Add(currWork);
                }
            }
            else if (inProgress.Count > 0 && _printSettings.workItem < inProgress.Count)
            {
                currWork = inProgress[_printSettings.workItem++];
            }

            return currWork;
        }
        #endregion

        #region private helpers
        /// <summary>
        /// Loads the print queue
        /// </summary>
        private void QueuePrintLines()
        {
            object[] items = new object[ToPrint.Count];
            ToPrint.CopyTo(items);
            _toPrint = new List<object>(items);
            inProgress = new List<object>();
            QueueManyItems();
        }

        /// <summary>
        /// Loads queue appropriate for a list of transactions
        /// </summary>
        private void QueueManyItems()
        {
            PageColumns = new Dictionary<PrintColumn, int>();
            int NumberOfPageParts = 1;
            int PrintAreaWidth = 0;

            if(pageSettings.Landscape)
                PrintAreaWidth = (int)pageSettings.PrintableArea.Height - pageSettings.Margins.Left - (pageSettings.Margins.Right);
            else
                PrintAreaWidth = (int)pageSettings.PrintableArea.Width - pageSettings.Margins.Left - (pageSettings.Margins.Right);
            int AllocatedWidth = 0;

            foreach (Column c in printfrom.Columns.VisibleItems)
            {
                int widthLeft = PrintAreaWidth - AllocatedWidth;

                if (widthLeft > c.Width)
                {
                    AllocatedWidth += c.Width;
                }
                else
                {
                    NumberOfPageParts++;
                    AllocatedWidth = c.Width;
                }

                if(c.ColumnItemAccessor != null)
                {

                    PageColumns.Add(new PrintColumn()
                    {
                        name = c.Name,
                        width = c.Width,
                        value = c.ColumnItemAccessor
                    }, NumberOfPageParts);
                }

                else if (c.ColumnItemAccessorWithParameter != null)
                {
                    PageColumns.Add(new PrintColumn()
                    {
                        name = c.Name,
                        width = c.Width,
                        value2 = c.ColumnItemAccessorWithParameter,
                        valueParameter = c.AccessorParameter
                    }, NumberOfPageParts);
                }

            }
        }
        #endregion        
    }
}
